/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { ArkClass, ts, AstTreeUtils } from 'arkanalyzer';
import { BaseChecker, BaseMetaData } from '../BaseChecker';
import { Rule, Defects, ClassMatcher, MatcherTypes, MatcherCallback } from '../../Index';
import { RuleListUtil } from '../../utils/common/DefectsList';
import { ClassCategory } from 'arkanalyzer/lib/core/model/ArkClass';
import { IssueReport } from '../../model/Defects';
const gMetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: 'docs/no-extraneous-class-check.md',
    description: 'Unexpected class with only static properties.'
};

interface ClassAnalysisResult {
    /** Whether to allow extraneous classes that contain only a constructor. */
    allowConstructorOnly?: boolean;
    /** Whether to allow extraneous classes that have no body (i.e. are empty). */
    allowEmpty?: boolean;
    /** Whether to allow extraneous classes that only contain static members. */
    allowStaticOnly?: boolean;
    /** Whether to allow extraneous classes that include a decorator. */
    allowWithDecorator?: boolean;
};
let allowConstructorOnly = false;
let allowEmpty = false;
let allowStaticOnly = false;
let allowWithDecorator = false;
export class NoExtraneousClassCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private buildMatcher: ClassMatcher = {
        matcherType: MatcherTypes.CLASS,
        isStatic: true
    };
    public registerMatchers(): MatcherCallback[] {
        const matchBuildCb: MatcherCallback = {
            matcher: this.buildMatcher,
            callback: this.check
        };
        return [matchBuildCb];
    };
    public check = (mArkClass: ArkClass) => {
        if (mArkClass.getCategory() !== ClassCategory.CLASS) {
            return;
        };
        let errMessages = this.metaData.description;
        const ruleOption = this.rule.option[0] as ClassAnalysisResult | undefined;
        this.setAllowFlags(ruleOption);
        const classCode = mArkClass.getCode();
        if (!classCode) {
            return;
        };
        const sourceFile = AstTreeUtils.getASTNode('NoExtraneousClassCheck.ts', classCode);
        if (!classCode || this.hasInstanceMembers(sourceFile) ||
            this.isClassExtending(classCode) ||
            this.handleOnlyConstructor(mArkClass, sourceFile, errMessages) ||
            this.handleEmptyClass(mArkClass, classCode) ||
            this.handleOnlyStaticMembers(sourceFile) ||
            this.handleDecorator(sourceFile)) {
            return;
        };
        let className = mArkClass.getName();
        if (/^%AC\$\d+$/.test(className)) {
            className = classCode;
        };
        this.addIssueReport(mArkClass, className, 2, errMessages);
    };
    private setAllowFlags(ruleOption: ClassAnalysisResult | undefined): void {
        allowConstructorOnly = ruleOption?.allowConstructorOnly ?? false;
        allowEmpty = ruleOption?.allowEmpty ?? false;
        allowStaticOnly = ruleOption?.allowStaticOnly ?? false;
        allowWithDecorator = ruleOption?.allowWithDecorator ?? false;
    };
    private hasInstanceMembers(sourceFile: ts.SourceFile): boolean {
        let hasInstanceMembers = false;
        sourceFile.forEachChild((node) => {
            if (ts.isClassDeclaration(node)) {
                hasInstanceMembers = this.hasClassInstanceMembers(node);
            };
        });
        return hasInstanceMembers;
    };
    private isClassExtending(classCode: string): boolean {
        const regex = /(?:export\s+)?class\s+\w+\s+extends\s+\w+/;
        return regex.test(classCode);
    };
    private handleOnlyConstructor(mArkClass: ArkClass, sourceFile: ts.SourceFile, errMessages: string): boolean {
        let isOnlyConstructor = false;
        sourceFile.forEachChild((node) => {
            if (this.isClassWithOnlyConstructor(node)) {
                isOnlyConstructor = true;
                errMessages = 'Unexpected class with only a constructor.';
            };
        });
        if (isOnlyConstructor && allowConstructorOnly) {
            return true;
        };
        return false;
    };

    private handleEmptyClass(mArkClass: ArkClass, classCode: string): boolean {
        const openBraceIndex = classCode.indexOf('{');
        const closeBraceIndex = classCode.lastIndexOf('}');
        if (openBraceIndex !== -1 && closeBraceIndex !== -1) {
            const classContent = classCode.substring(openBraceIndex + 1, closeBraceIndex).trim();
            if (classContent.length === 0) {
                if (allowEmpty) {
                    return true;
                } else {
                    const errMessages = 'Unexpected empty class.';
                    this.addIssueReport(mArkClass, mArkClass.getName(), this.metaData.severity, errMessages);
                    return true;
                };
            };
        };
        return false;
    };
    private handleOnlyStaticMembers(sourceFile: ts.SourceFile): boolean {
        let isOnlyStatic = false;
        sourceFile.forEachChild((node) => {
            if (ts.isClassDeclaration(node)) {
                isOnlyStatic = this.isClassWithOnlyStaticMembers(node);
            };
        });
        if (isOnlyStatic && allowStaticOnly) {
            return true;
        };
        return false;
    };
    private handleDecorator(sourceFile: ts.SourceFile): boolean {
        let isDecorator = false;
        sourceFile.forEachChild((node) => {
            if (this.hasDecorators(node)) {
                isDecorator = true;
            };
        });
        if (isDecorator && allowWithDecorator) {
            return true;
        };
        return false;
    };
    private isClassWithOnlyStaticMembers(node: ts.ClassDeclaration): boolean {
        const members = node.members;
        for (const member of members) {
            if (
                (ts.isPropertyDeclaration(member) && !ts.getModifiers(member)?.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword)) ||
                (ts.isMethodDeclaration(member) && !ts.getModifiers(member)?.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword))
            ) {
                if (ts.isConstructorDeclaration(member) && allowConstructorOnly) {
                    return false;
                };
                return false;
            };
        };
        return true;
    };
    private hasClassInstanceMembers(node: ts.ClassDeclaration): boolean {
        const members = node.members;
        let isStatic = false;
        for (const member of members) {
            if (
                (ts.isPropertyDeclaration(member) && !ts.getModifiers(member)?.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword)) ||
                (ts.isMethodDeclaration(member) && !ts.getModifiers(member)?.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword))
            ) {
                isStatic = true;
            };
        };
        const constructor = members.find(ts.isConstructorDeclaration);
        if (constructor) {
            for (const parameter of constructor.parameters) {
                if (!ts.getModifiers(parameter)?.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword)) {
                    const decorators = ts.canHaveDecorators(node) ? ts.getDecorators(node) : undefined;
                    if (decorators && decorators.length > 0 && !allowWithDecorator) {
                        return false;
                    };
                    return true;
                };
            };
        };
        if (isStatic) {
            return true;
        };
        return false;
    };
    private hasDecorators(node: ts.Node): boolean {
        const decorators = ts.canHaveDecorators(node) ? ts.getDecorators(node) : undefined;
        if (decorators && decorators.length > 0) {
            return true;
        };
        return false;
    };
    private isClassWithOnlyConstructor(node: ts.Node): boolean {
        if (!ts.isClassDeclaration(node)) {
            return false;
        };
        const members = node.members;
        const hasConstructor = members.some(ts.isConstructorDeclaration);
        const hasOtherMembers = members.some(
            (member) => !ts.isConstructorDeclaration(member)
        );
        if (hasConstructor && !hasOtherMembers) {
            const constructor = members.find(ts.isConstructorDeclaration) as ts.ConstructorDeclaration;
            return this.isConstructorEmpty(constructor);
        };
        return false;
    };
    private isConstructorEmpty(constructor: ts.ConstructorDeclaration): boolean {
        if (!constructor.body) {
            return true;
        };
        const constructorBody = constructor.body;
        if (constructorBody.statements.length === 1) {
            const statement = constructorBody.statements[0];
            return this.isConsoleLog(statement);
        };
        return constructor.body.statements.length === 0;
    };
    private isConsoleLog(node: ts.Node): boolean {
        const text = node.getText();
        const regex = /\bconsole\s*\.\s*\w+\b/g;
        if (regex.test(text)) {
            return true;
        };
        return false;
    };
    private addIssueReport(mArkClass: ArkClass, name: string, severity: number, errMessages: string): void {
        const warnInfo = this.getLineAndColumn(mArkClass, name);
        const defects = new Defects(warnInfo.line, warnInfo.startCol, warnInfo.endCol, errMessages, severity, this.rule.ruleId,
            warnInfo.filePath, this.metaData.ruleDocPath, true, false, false, false);
        this.issues.push(new IssueReport(defects, undefined));
        RuleListUtil.push(defects);
    };
    private getLineAndColumn(mArkClass: ArkClass, name: string): { line: number, startCol: number, endCol: number, filePath: string } {
        const originText = mArkClass.getCode();
        if (!originText) {
            return { line: -1, startCol: -1, endCol: -1, filePath: '' };
        };
        const returnIndex = originText.indexOf(name);
        const substring = originText.substring(0, returnIndex);
        const newlineCount = substring.split("\r\n").length - 1;
        const line = mArkClass.getLine() + newlineCount;
        const pos = originText?.indexOf(name);
        if (pos !== -1) {
            let startCol = pos + 1;
            const lastNewLineIndex = originText.lastIndexOf('\r\n', returnIndex - 1);
            if (lastNewLineIndex !== -1) {
                startCol = returnIndex - lastNewLineIndex - 2;
            };
            const endCol = startCol + name.length - 1;
            const originPath = mArkClass.getDeclaringArkFile().getFilePath();
            return { line, startCol, endCol, filePath: originPath };
        };
        return { line: -1, startCol: -1, endCol: -1, filePath: '' };
    };
};